package com.gmail.volodymyrdotsenko.roo.webeasy.scaffold;

import static org.springframework.roo.classpath.customdata.CustomDataKeys.COUNT_ALL_METHOD;
import static org.springframework.roo.classpath.customdata.CustomDataKeys.FIND_ALL_SORTED_METHOD;
import static org.springframework.roo.classpath.customdata.CustomDataKeys.FIND_ENTRIES_SORTED_METHOD;
import static org.springframework.roo.classpath.customdata.CustomDataKeys.FIND_ALL_METHOD;
import static org.springframework.roo.classpath.customdata.CustomDataKeys.FIND_ENTRIES_METHOD;
import static org.springframework.roo.classpath.customdata.CustomDataKeys.FIND_METHOD;
import static org.springframework.roo.classpath.customdata.CustomDataKeys.MERGE_METHOD;
import static org.springframework.roo.classpath.customdata.CustomDataKeys.PERSIST_METHOD;
import static org.springframework.roo.classpath.customdata.CustomDataKeys.REMOVE_METHOD;
import static org.springframework.roo.model.JavaType.INT_OBJECT;
import static org.springframework.roo.model.JavaType.STRING;
import static org.springframework.roo.model.JavaType.VOID_PRIMITIVE;
import static org.springframework.roo.model.JdkJavaType.ARRAYS;
import static org.springframework.roo.model.JdkJavaType.ARRAY_LIST;
import static org.springframework.roo.model.JdkJavaType.LIST;
import static org.springframework.roo.model.JdkJavaType.UNSUPPORTED_ENCODING_EXCEPTION;
import static org.springframework.roo.model.Jsr303JavaType.VALID;
import static org.springframework.roo.model.SpringJavaType.AUTOWIRED;
import static org.springframework.roo.model.SpringJavaType.BINDING_RESULT;
import static org.springframework.roo.model.SpringJavaType.CONVERSION_SERVICE;
import static org.springframework.roo.model.SpringJavaType.LOCALE_CONTEXT_HOLDER;
import static org.springframework.roo.model.SpringJavaType.MODEL;
import static org.springframework.roo.model.SpringJavaType.PATH_VARIABLE;
import static org.springframework.roo.model.SpringJavaType.REQUEST_MAPPING;
import static org.springframework.roo.model.SpringJavaType.REQUEST_METHOD;
import static org.springframework.roo.model.SpringJavaType.REQUEST_PARAM;
import static org.springframework.roo.model.SpringJavaType.URI_UTILS;
import static org.springframework.roo.model.SpringJavaType.WEB_UTILS;

import java.lang.reflect.Modifier;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.SortedMap;
import java.util.logging.Logger;

import org.apache.commons.lang3.Validate;
import org.apache.commons.lang3.builder.ToStringBuilder;
import org.springframework.roo.addon.web.mvc.controller.details.DateTimeFormatDetails;
import org.springframework.roo.addon.web.mvc.controller.details.JavaTypeMetadataDetails;
import org.springframework.roo.addon.web.mvc.controller.details.JavaTypePersistenceMetadataDetails;
import org.springframework.roo.classpath.PhysicalTypeIdentifierNamingUtils;
import org.springframework.roo.classpath.PhysicalTypeMetadata;
import org.springframework.roo.classpath.TypeLocationService;
import org.springframework.roo.classpath.customdata.tagkeys.MethodMetadataCustomDataKey;
import org.springframework.roo.classpath.details.FieldMetadata;
import org.springframework.roo.classpath.details.MethodMetadata;
import org.springframework.roo.classpath.details.MethodMetadataBuilder;
import org.springframework.roo.classpath.details.annotations.AnnotatedJavaType;
import org.springframework.roo.classpath.details.annotations.AnnotationAttributeValue;
import org.springframework.roo.classpath.details.annotations.AnnotationMetadataBuilder;
import org.springframework.roo.classpath.details.annotations.EnumAttributeValue;
import org.springframework.roo.classpath.details.annotations.StringAttributeValue;
import org.springframework.roo.classpath.itd.AbstractItdTypeDetailsProvidingMetadataItem;
import org.springframework.roo.classpath.itd.InvocableMemberBodyBuilder;
import org.springframework.roo.classpath.layers.MemberTypeAdditions;
import org.springframework.roo.metadata.MetadataIdentificationUtils;
import org.springframework.roo.model.EnumDetails;
import org.springframework.roo.model.JavaSymbolName;
import org.springframework.roo.model.JavaType;
import org.springframework.roo.project.LogicalPath;
import org.springframework.roo.support.logging.HandlerUtils;

public class WebeasyScaffoldMetadata extends
		AbstractItdTypeDetailsProvidingMetadataItem {

	private static Logger LOGGER = HandlerUtils
			.getLogger(WebeasyScaffoldMetadata.class);

	// Constants
	public static final JavaType ROO_ROO_WEBEASY_SCAFFOLD = new JavaType(
			"com.gmail.volodymyrdotsenko.roo.webeasy.scaffold.RooWebeasyScaffold");

	private static final String PROVIDES_TYPE_STRING = WebeasyScaffoldMetadata.class
			.getName();
	private static final String PROVIDES_TYPE = MetadataIdentificationUtils
			.create(PROVIDES_TYPE_STRING);

	private static final JavaSymbolName CS_FIELD = new JavaSymbolName(
			"conversionService");
	private static final JavaType HTTP_SERVLET_REQUEST = new JavaType(
			"javax.servlet.http.HttpServletRequest");
	private static final StringAttributeValue PRODUCES_HTML = new StringAttributeValue(
			new JavaSymbolName("produces"), "text/html");

	public static final String getMetadataIdentiferType() {
		return PROVIDES_TYPE;
	}

	public static final String createIdentifier(JavaType javaType,
			LogicalPath path) {

		return PhysicalTypeIdentifierNamingUtils.createIdentifier(
				PROVIDES_TYPE_STRING, javaType, path);
	}

	public static final JavaType getJavaType(String metadataIdentificationString) {
		return PhysicalTypeIdentifierNamingUtils.getJavaType(
				PROVIDES_TYPE_STRING, metadataIdentificationString);
	}

	public static final LogicalPath getPath(String metadataIdentificationString) {
		return PhysicalTypeIdentifierNamingUtils.getPath(PROVIDES_TYPE_STRING,
				metadataIdentificationString);
	}

	public static boolean isValid(String metadataIdentificationString) {
		return PhysicalTypeIdentifierNamingUtils.isValid(PROVIDES_TYPE_STRING,
				metadataIdentificationString);
	}

	private boolean compositePk = false;

	private WebeasyScaffoldAnnotationValues annotationValues;
	private String controllerPath;
	private JavaType formBackingType;
	private Map<JavaSymbolName, DateTimeFormatDetails> dateTypes;
	private String entityName;
	private JavaTypeMetadataDetails javaTypeMetadataHolder;
	private TypeLocationService typeLocationService;

	protected WebeasyScaffoldMetadata(
			String identifier,
			JavaType aspectName,
			PhysicalTypeMetadata governorPhysicalTypeMetadata,
			final WebeasyScaffoldAnnotationValues annotationValues,
			final FieldMetadata idField,
			final SortedMap<JavaType, JavaTypeMetadataDetails> specialDomainTypes,
			final List<JavaTypeMetadataDetails> dependentTypes,
			final Map<JavaSymbolName, DateTimeFormatDetails> dateTypes,
			final Map<MethodMetadataCustomDataKey, MemberTypeAdditions> crudAdditions,
			final Collection<JavaType> editableFieldTypes,
			TypeLocationService typeLocationService) {

		super(identifier, aspectName, governorPhysicalTypeMetadata);

		LOGGER.info("WebeasyScaffoldMetadata constructed");

		Validate.notNull(annotationValues, "Annotation values required");

		this.annotationValues = annotationValues;
		this.typeLocationService = typeLocationService;
		this.controllerPath = annotationValues.getPath();
		this.dateTypes = dateTypes;
		this.formBackingType = annotationValues.getFormBackingObject();

		this.entityName = JavaSymbolName.getReservedWordSafeName(
				formBackingType).getSymbolName();
		this.javaTypeMetadataHolder = specialDomainTypes.get(formBackingType);

		// "create" methods
		final MemberTypeAdditions persistMethod = crudAdditions
				.get(PERSIST_METHOD);
		// if (annotationValues.isCreate() && persistMethod != null) {
		builder.addMethod(getCreateMethod(persistMethod));
		// builder.addMethod(getCreateFormMethod(dependentTypes));
		persistMethod.copyAdditionsTo(builder, governorTypeDetails);
		// }

		final MemberTypeAdditions countAllMethod = crudAdditions
				.get(COUNT_ALL_METHOD);
		final MemberTypeAdditions findMethod = crudAdditions.get(FIND_METHOD);
		final MemberTypeAdditions findAllMethod = crudAdditions
				.get(FIND_ALL_METHOD);
		final MemberTypeAdditions findEntriesMethod = crudAdditions
				.get(FIND_ENTRIES_METHOD);
		final MemberTypeAdditions findAllSortedMethod = crudAdditions
				.get(FIND_ALL_SORTED_METHOD);
		final MemberTypeAdditions findEntriesSortedMethod = crudAdditions
				.get(FIND_ENTRIES_SORTED_METHOD);

		// if (annotationValues.isCreate() || annotationValues.isUpdate()) {
		builder.addMethod(getPopulateEditFormMethod(formBackingType,
				specialDomainTypes.values(), editableFieldTypes));
		builder.addMethod(getEncodeUrlPathSegmentMethod());
		// }

		itdTypeDetails = builder.build();

	}

	private MethodMetadataBuilder getCreateMethod(
			final MemberTypeAdditions persistMethod) {
		final JavaTypePersistenceMetadataDetails javaTypePersistenceMetadataHolder = javaTypeMetadataHolder
				.getPersistenceDetails();
		if (javaTypePersistenceMetadataHolder == null
				|| javaTypePersistenceMetadataHolder
						.getIdentifierAccessorMethod() == null) {
			return null;
		}

		final JavaSymbolName methodName = new JavaSymbolName("create");
		if (governorHasMethodWithSameName(methodName)) {
			return null;
		}

		final AnnotationMetadataBuilder validAnnotation = new AnnotationMetadataBuilder(
				VALID);

		final List<AnnotatedJavaType> parameterTypes = Arrays
				.asList(new AnnotatedJavaType(formBackingType, validAnnotation
						.build()), new AnnotatedJavaType(BINDING_RESULT),
						new AnnotatedJavaType(MODEL), new AnnotatedJavaType(
								HTTP_SERVLET_REQUEST));
		final List<JavaSymbolName> parameterNames = Arrays.asList(
				new JavaSymbolName(entityName), new JavaSymbolName(
						"bindingResult"), new JavaSymbolName("uiModel"),
				new JavaSymbolName("httpServletRequest"));

		final List<AnnotationAttributeValue<?>> requestMappingAttributes = new ArrayList<AnnotationAttributeValue<?>>();
		requestMappingAttributes.add(new EnumAttributeValue(new JavaSymbolName(
				"method"), new EnumDetails(REQUEST_METHOD, new JavaSymbolName(
				"POST"))));
		requestMappingAttributes.add(PRODUCES_HTML);
		final AnnotationMetadataBuilder requestMapping = new AnnotationMetadataBuilder(
				REQUEST_MAPPING, requestMappingAttributes);
		final List<AnnotationMetadataBuilder> annotations = new ArrayList<AnnotationMetadataBuilder>();
		annotations.add(requestMapping);

		final InvocableMemberBodyBuilder bodyBuilder = new InvocableMemberBodyBuilder();
		bodyBuilder.appendFormalLine("if (bindingResult.hasErrors()) {");
		bodyBuilder.indent();
		bodyBuilder.appendFormalLine("populateEditForm(uiModel, " + entityName
				+ ");");
		bodyBuilder.appendFormalLine("return \"" + controllerPath
				+ "/create\";");
		bodyBuilder.indentRemove();
		bodyBuilder.appendFormalLine("}");
		bodyBuilder.appendFormalLine("uiModel.asMap().clear();");
		bodyBuilder.appendFormalLine(persistMethod.getMethodCall() + ";");
		bodyBuilder.appendFormalLine("return \"redirect:/"
				+ controllerPath
				+ "/\" + encodeUrlPathSegment("
				+ (compositePk ? "conversionService.convert(" : "")
				+ entityName
				+ "."
				+ javaTypePersistenceMetadataHolder
						.getIdentifierAccessorMethod().getMethodName() + "()"
				+ (compositePk ? ", String.class)" : ".toString()")
				+ ", httpServletRequest);");

		final MethodMetadataBuilder methodBuilder = new MethodMetadataBuilder(
				getId(), Modifier.PUBLIC, methodName, STRING, parameterTypes,
				parameterNames, bodyBuilder);
		methodBuilder.setAnnotations(annotations);
		return methodBuilder;
	}

	private MethodMetadata getPopulateEditFormMethod(final JavaType entity,
			final Collection<JavaTypeMetadataDetails> specialDomainTypes,
			final Collection<JavaType> editableFieldTypes) {

		final JavaSymbolName methodName = new JavaSymbolName("populateEditForm");
		final JavaType[] parameterTypes = { MODEL, entity };
		final List<JavaSymbolName> parameterNames = Arrays.asList(
				new JavaSymbolName("uiModel"), new JavaSymbolName(entityName));
		if (governorHasMethod(methodName, parameterTypes)) {
			return null;
		}
		final InvocableMemberBodyBuilder bodyBuilder = new InvocableMemberBodyBuilder();
		bodyBuilder.appendFormalLine("uiModel.addAttribute(\"" + entityName
				+ "\", " + entityName + ");");
		if (!dateTypes.isEmpty()) {
			bodyBuilder.appendFormalLine("addDateTimeFormatPatterns(uiModel);");
		}
		// if (annotationValues.isPopulateMethods()) {
		for (final JavaTypeMetadataDetails domainType : specialDomainTypes) {
			if (editableFieldTypes.contains(domainType.getJavaType())) {
				final JavaTypePersistenceMetadataDetails persistenceDetails = domainType
						.getPersistenceDetails();
				final String modelAttribute = domainType.getPlural()
						.toLowerCase();
				if (persistenceDetails != null
						&& persistenceDetails.getFindAllMethod() != null) {
					bodyBuilder.appendFormalLine("uiModel.addAttribute(\""
							+ modelAttribute
							+ "\", "
							+ persistenceDetails.getFindAllMethod()
									.getMethodCall() + ");");
					persistenceDetails.getFindAllMethod().copyAdditionsTo(
							builder, governorTypeDetails);
				} else if (domainType.isEnumType()) {
					bodyBuilder.appendFormalLine("uiModel.addAttribute(\""
							+ modelAttribute + "\", " + getShortName(ARRAYS)
							+ ".asList("
							+ getShortName(domainType.getJavaType())
							+ ".values())" + ");");
				}
			}
		}
		// }
		return new MethodMetadataBuilder(getId(), 0, methodName,
				VOID_PRIMITIVE,
				AnnotatedJavaType.convertFromJavaTypes(parameterTypes),
				parameterNames, bodyBuilder).build();
	}

	private String getShortName(final JavaType type) {
		return type.getNameIncludingTypeParameters(false,
				builder.getImportRegistrationResolver());
	}

	private MethodMetadataBuilder getEncodeUrlPathSegmentMethod() {
		final JavaSymbolName methodName = new JavaSymbolName(
				"encodeUrlPathSegment");
		if (governorHasMethodWithSameName(methodName)) {
			return null;
		}

		final List<JavaType> parameterTypes = Arrays.asList(STRING,
				HTTP_SERVLET_REQUEST);
		final List<JavaSymbolName> parameterNames = Arrays.asList(
				new JavaSymbolName("pathSegment"), new JavaSymbolName(
						"httpServletRequest"));

		builder.getImportRegistrationResolver().addImport(
				UNSUPPORTED_ENCODING_EXCEPTION);

		final InvocableMemberBodyBuilder bodyBuilder = new InvocableMemberBodyBuilder();
		bodyBuilder
				.appendFormalLine("String enc = httpServletRequest.getCharacterEncoding();");
		bodyBuilder.appendFormalLine("if (enc == null) {");
		bodyBuilder.indent();
		bodyBuilder.appendFormalLine("enc = " + getShortName(WEB_UTILS)
				+ ".DEFAULT_CHARACTER_ENCODING;");
		bodyBuilder.indentRemove();
		bodyBuilder.appendFormalLine("}");
		bodyBuilder.appendFormalLine("try {");
		bodyBuilder.indent();
		bodyBuilder.appendFormalLine("pathSegment = " + getShortName(URI_UTILS)
				+ ".encodePathSegment(pathSegment, enc);");
		bodyBuilder.indentRemove();
		bodyBuilder.appendFormalLine("} catch ("
				+ getShortName(UNSUPPORTED_ENCODING_EXCEPTION) + " uee) {}");
		bodyBuilder.appendFormalLine("return pathSegment;");

		return new MethodMetadataBuilder(getId(), 0, methodName, STRING,
				AnnotatedJavaType.convertFromJavaTypes(parameterTypes),
				parameterNames, bodyBuilder);
	}

	public String toString() {
		final ToStringBuilder builder = new ToStringBuilder(this);
		builder.append("identifier", getId());
		builder.append("valid", valid);
		builder.append("aspectName", aspectName);
		builder.append("destinationType", destination);
		builder.append("governor", governorPhysicalTypeMetadata.getId());
		builder.append("itdTypeDetails", itdTypeDetails);
		return builder.toString();
	}
}